package com.vst.orm.dao;

import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class DAOUtil {
	public static <T> List<Class<?>> getTypeArguments(Class<T> baseClass, Class<? extends T> childClass) {
		Map<Type, Type> resolvedTypes = new HashMap();
		Type type = childClass;
		while (!getClass(type).equals(baseClass)) {
			if ((type instanceof Class)) {
				type = ((Class) type).getGenericSuperclass();
			} else {
				ParameterizedType parameterizedType = (ParameterizedType) type;
				Class<?> rawType = (Class) parameterizedType.getRawType();

				Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();

				TypeVariable<?>[] typeParameters = rawType.getTypeParameters();
				for (int i = 0; i < actualTypeArguments.length; i++) {
					resolvedTypes.put(typeParameters[i], actualTypeArguments[i]);
				}
				if (!rawType.equals(baseClass)) {
					type = rawType.getGenericSuperclass();
				}
			}
		}
		Type[] actualTypeArguments;
		if ((type instanceof Class)) {
			actualTypeArguments = ((Class) type).getTypeParameters();
		} else {
			actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments();
		}
		List<Class<?>> typeArgumentsAsClasses = new ArrayList();
		for (Type baseType : actualTypeArguments) {
			while (resolvedTypes.containsKey(baseType)) {
				baseType = (Type) resolvedTypes.get(baseType);
			}
			typeArgumentsAsClasses.add(getClass(baseType));
		}
		return typeArgumentsAsClasses;
	}

	private static Class<?> getClass(Type type) {
		if ((type instanceof Class)) {
			return (Class) type;
		}
		if ((type instanceof ParameterizedType)) {
			return getClass(((ParameterizedType) type).getRawType());
		}
		if ((type instanceof GenericArrayType)) {
			Type componentType = ((GenericArrayType) type).getGenericComponentType();

			Class<?> componentClass = getClass(componentType);
			if (componentClass != null) {
				return Array.newInstance(componentClass, 0).getClass();
			}
			return null;
		}
		return null;
	}

	public static Object callMethod(Object object, String methodName, Object... args)
			throws NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
		Class<?>[] paramTypes = new Class[args.length];
		for (int i = 0; i < args.length; i++) {
			if (args[i] == null) {
				throw new NullPointerException(
						"No arguments may be null when using callMethod(Object, String, Object...) because every argument is needed in order to determine the parameter types. Use callMethod(Object, String, Class<?>[], Object...) instead and specify parameter types.");
			}
			paramTypes[i] = args[i].getClass();
		}
		return callMethod(object, methodName, paramTypes, args);
	}

	public static Object callMethod(Object object, String methodName, Class<?>[] paramTypes, Object... args)
			throws NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
		Method method = getMethod(object.getClass(), methodName, paramTypes);
		if (method == null) {
			throw new NoSuchMethodException("Method: " + methodName + " not found on Class: " + object.getClass());
		}
		if (method.isVarArgs()) {
			Object[] allargs = new Object[method.getParameterTypes().length];
			Object[] vargs = (Object[]) Array.newInstance(
					method.getParameterTypes()[(method.getParameterTypes().length - 1)].getComponentType(),
					args.length - method.getParameterTypes().length + 1);
			for (int i = 0; i < method.getParameterTypes().length - 1; i++) {
				allargs[i] = args[i];
			}
			for (int i = 0; i < args.length - method.getParameterTypes().length + 1; i++) {
				vargs[i] = args[(method.getParameterTypes().length - 1 + i)];
			}
			allargs[(method.getParameterTypes().length - 1)] = vargs;

			return method.invoke(object, allargs);
		}
		return method.invoke(object, args);
	}

	public static Method getMethod(Class<?> klass, String methodName, Class<?>... paramTypes) {
		List<Method> candidates = new ArrayList();
		label285: for (Method method : klass.getMethods()) {
			if (method.getName().equals(methodName)) {
				Class<?>[] methodParamTypes = method.getParameterTypes();
				if ((paramTypes.length == methodParamTypes.length)
						|| ((method.isVarArgs()) && (paramTypes.length >= methodParamTypes.length - 1))) {
					if (method.isVarArgs()) {
						for (int i = 0; i < methodParamTypes.length - 1; i++) {
							if ((paramTypes[i] != null) && (!methodParamTypes[i].isAssignableFrom(paramTypes[i]))) {
								break label285;
							}
						}
						if (methodParamTypes.length != paramTypes.length + 1) {
							if ((methodParamTypes.length != paramTypes.length)
									|| (!methodParamTypes[(paramTypes.length - 1)]
											.isAssignableFrom(paramTypes[(paramTypes.length - 1)]))) {
								Class<?> varClass = methodParamTypes[(methodParamTypes.length - 1)].getComponentType();
								for (int i = methodParamTypes.length - 1; i < paramTypes.length; i++) {
									if ((paramTypes[i] != null) && (!varClass.isAssignableFrom(paramTypes[i]))) {
										break label285;
									}
								}
							}
						}
					} else {
						for (int i = 0; i < methodParamTypes.length; i++) {
							if ((paramTypes[i] != null) && (!methodParamTypes[i].isAssignableFrom(paramTypes[i]))) {
								break label285;
							}
						}
					}
					candidates.add(method);
				}
			}
		}
		if (candidates.size() == 0) {
			return null;
		}
		if (candidates.size() == 1) {
			return (Method) candidates.get(0);
		}
		Iterator<Method> itr = candidates.iterator();
		while (itr.hasNext()) {
			Method m = (Method) itr.next();
			if (m.isVarArgs()) {
				if ((m.getParameterTypes().length != paramTypes.length)
						|| (!m.getParameterTypes()[(paramTypes.length - 1)]
								.isAssignableFrom(paramTypes[(paramTypes.length - 1)]))) {
					itr.remove();
				}
			}
		}
		if (candidates.size() == 0) {
			return null;
		}
		Method a = (Method) candidates.get(0);
		boolean ambiguous = false;
		for (int j = 1; j < candidates.size(); j++) {
			Method b = (Method) candidates.get(j);

			Class<?>[] aTypes = a.getParameterTypes();
			Class<?>[] bTypes = b.getParameterTypes();

			int aScore = 0;
			int bScore = 0;
			for (int i = 0; i < aTypes.length; i++) {
				if (aTypes[i] != null) {
					int distA = getDist(aTypes[i], paramTypes[i]);
					int distB = getDist(bTypes[i], paramTypes[i]);
					if (distA > distB) {
						bScore++;
					} else if (distA < distB) {
						aScore++;
					} else if (distA == 1000) {
						if (!aTypes[i].equals(bTypes[i])) {
							if (aTypes[i].isAssignableFrom(bTypes[i])) {
								bScore++;
							} else if (bTypes[i].isAssignableFrom(aTypes[i])) {
								aScore++;
							}
						}
					}
				}
			}
			if (aScore == bScore) {
				ambiguous = true;
			} else if (bScore > aScore) {
				a = b;
				ambiguous = false;
			}
		}
		if (ambiguous) {
			return null;
		}
		return a;
	}

	private static int getDist(Class<?> superClass, Class<?> klass) {
		if (klass.isArray()) {
			if (superClass.isArray()) {
				superClass = superClass.getComponentType();
				klass = klass.getComponentType();
			} else {
				return 3000;
			}
		}
		if (superClass.equals(klass)) {
			return 0;
		}
		if (superClass.equals(Object.class)) {
			return 2000;
		}
		if (superClass.isInterface()) {
			return 1000;
		}
		int dist = 0;
		do {
			dist++;
			klass = klass.getSuperclass();
		} while (!superClass.equals(klass));
		return dist;
	}
}
